﻿using System;
using Oxage.CNC.Models;

namespace Oxage.Simulator
{
	[InlineBinding(typeof(IArcInline))]
	public class ArcStepMaker : IStepMaker
	{
		#region Members
		public struct ArcParameters
		{
			public int x, y;        //current point
			public int dx, dy;      //change (-1, 0, +1)
			public int px, py;      //previous point
			public int sx, sy;      //starting point
			public int dir;         //-1 if clockwise, otherwise +1
			public int radius;      //radius
			public double angle;    //current angle in degrees
		}
		#endregion

		#region Protected methods
		protected void ArcPoint(ref ArcParameters p)
		{
			p.dx = 0;
			p.dy = 0;

			p.angle = Math.Atan2(p.y, p.x) * 180 / Math.PI;
			if (p.angle < 0) p.angle += 360; //convert negative to positive degrees

			if (0 <= p.angle && p.angle < 45)
			{
				p.y += p.dir;
				p.dy = p.dir;
				p.px = p.x;
				p.x = (int)Math.Round(Math.Sqrt(p.radius * p.radius - p.y * p.y));

				if (p.x != p.px) p.dx = -p.dir;
				else p.dx = 0;
			}
			else if (45 <= p.angle && p.angle < 90)
			{
				p.x -= p.dir;
				p.dx = -p.dir;
				p.py = p.y;
				p.y = (int)Math.Round(Math.Sqrt(p.radius * p.radius - p.x * p.x));

				if (p.y != p.py) p.dy = p.dir;
				else p.dy = 0;
			}
			else if (90 <= p.angle && p.angle < 135)
			{
				p.x -= p.dir;
				p.dx = -p.dir;
				p.py = p.y;
				p.y = (int)Math.Round(Math.Sqrt(p.radius * p.radius - p.x * p.x));

				if (p.y != p.py) p.dy = -p.dir;
				else p.dy = 0;
			}
			else if (135 <= p.angle && p.angle < 180)
			{
				p.y -= p.dir;
				p.dy = -p.dir;
				p.px = p.x;
				p.x = -(int)Math.Round(Math.Sqrt(p.radius * p.radius - p.y * p.y));

				if (p.x != p.px) p.dx = -p.dir;
				else p.dx = 0;
			}
			else if (180 <= p.angle && p.angle < 225)
			{
				p.y -= p.dir;
				p.dy = -p.dir;
				p.px = p.x;
				p.x = -(int)Math.Round(Math.Sqrt(p.radius * p.radius - p.y * p.y));

				if (p.x != p.px) p.dx = p.dir;
				else p.dx = 0;
			}
			else if (225 <= p.angle && p.angle < 270)
			{
				p.x += p.dir;
				p.dx = p.dir;
				p.py = p.y;
				p.y = -(int)Math.Round(Math.Sqrt(p.radius * p.radius - p.x * p.x));

				if (p.y != p.py) p.dy = -p.dir;
				else p.dy = 0;
			}
			else if (270 <= p.angle && p.angle < 315)
			{
				p.x += p.dir;
				p.dx = p.dir;
				p.py = p.y;
				p.y = -(int)Math.Round(Math.Sqrt(p.radius * p.radius - p.x * p.x));

				if (p.y != p.py) p.dy = p.dir;
				else p.dy = 0;
			}
			else if (315 <= p.angle && p.angle < 360)
			{
				p.y += p.dir;
				p.dy = p.dir;
				p.px = p.x;
				p.x = (int)Math.Round(Math.Sqrt(p.radius * p.radius - p.y * p.y));

				if (p.x != p.px) p.dx = p.dir;
				else p.dx = 0;
			}
		}
		#endregion

		#region Public methods
		/// <summary>
		/// 2-dimensional circle, positive direction only, angle in degrees<br />
		/// (under construction)<br />
		/// todo: full circle, if startAngle > endAngle then clockwise (no bool clockwise parameter needed)
		/// </summary>
		/// <param name="radius">Radius, distance from center of the arc</param>
		/// <param name="startAngle">Starting angle in degrees</param>
		/// <param name="endAngle">Ending angle in degrees</param>
		/// <param name="clockwise">True if clockwise, otherwise false</param>
		public void Arc(IStepDriver driver, int radius, double startAngle, double endAngle, bool clockwise)
		{
			//calculate starting point from radius and starting angle
			ArcParameters p = new ArcParameters();
			p.x = (int)Math.Round(radius * Math.Cos(startAngle * Math.PI / 180));
			p.y = (int)Math.Round(radius * Math.Sin(startAngle * Math.PI / 180));
			p.sx = p.x;
			p.sy = p.y;
			p.radius = radius;

			//is circle?
			bool isCircle = false;
			if (Math.Abs(Math.Abs(startAngle) - Math.Abs(endAngle)) >= 360)
			{
				isCircle = true;
			}

			//convert starting angle to range 0 to 360 degrees
			if (startAngle < 0)
			{
				startAngle %= 360;
				startAngle += 360;
			}
			else if (startAngle >= 360)
			{
				startAngle %= 360;
			}

			//convert ending angle to range 0 to 360 degrees
			if (isCircle)
			{
				endAngle = startAngle + (clockwise ? -360 : 360); //TODO: 360, full circle
			}
			if (endAngle < 0)
			{
				endAngle %= 360;
				endAngle += 360;
			}
			else if (endAngle > 360)
			{
				endAngle %= 360;
			}

			//arc algorithm
			p.angle = startAngle;
			if (clockwise)
			{
				p.dir = -1;
				//TODO: Handle condition (p.angle < endAngle)
				while (p.angle >= endAngle)
				{
					ArcPoint(ref p);
					driver.Step(new NPoint(p.dx, p.dy, 0));
					if (p.x == p.sx && p.y == p.sy) break; //360
				}
			}
			else
			{
				p.dir = +1;
				//TODO: Handle condition (p.angle > endAngle)
				while (p.angle <= endAngle)
				{
					ArcPoint(ref p);
					driver.Step(new NPoint(p.dx, p.dy, 0));
					if (p.x == p.sx && p.y == p.sy) break; //360
				}
			}
		}
		#endregion

		#region IStepMaker Members
		public void Run(IStepDriver driver, object data)
		{
			if (driver == null)
			{
				throw new ArgumentNullException("driver");
			}

			var inline = data as IArcInline;
			if (inline == null)
			{
				throw new ArgumentException("Expected IArcInline");
			}

			Arc(driver, inline.Radius, inline.StartAngle, inline.EndAngle, inline.Clockwise);
		}
		#endregion
	}
}
